package com.idunnolol.sotm.data; import android.content.Context; import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; import android.preference.PreferenceManager; import android.util.JsonReader; import android.util.Pair; import android.util.SparseIntArray; import com.danlew.utils.Log; import com.danlew.utils.ResourceUtils; import com.idunnolol.sotm.R; import com.idunnolol.sotm.data.Card.Type; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; public class Db { private static final Db sInstance = new Db(); // Only show advanced villains who have more than a certain # of games logged private static final int ADVANCED_COUNT_CUTOFF = 35; private Db() { // Singleton } public static void init(Context context) { try { long start = System.nanoTime(); // Read in data from assets sInstance.initCards(context); sInstance.initNameConversions(context); sInstance.initPoints(context); // If we have saved card state settings, use them; otherwise use // the defaults SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); if (prefs.contains(PREFERENCE_CARD_STATE)) { Set<String> enabledIds = prefs.getStringSet(PREFERENCE_CARD_STATE, null); for (String id : enabledIds) { Card card = sInstance.mCards.get(id); // Card can be null if an advanced card ends up being cut off later if (card != null) { sInstance.mCards.get(id).setEnabled(true); } } } else { for (CardSet cardSet : sInstance.mCardSets) { cardSet.setAllCardsEnabled(cardSet.isEnabledByDefault()); } saveCardStates(context); } long end = System.nanoTime(); Log.i("Initialized db in " + ((end - start) / 100000) + " ms"); } catch (IOException e) { Log.e("Error while reading data", e); throw new RuntimeException(e); } } ////////////////////////////////////////////////////////////////////////// // Data private static final String PREFERENCE_CARD_STATE = "com.idunnolol.sotm.cards.state"; private Map<String, Card> mCards = new HashMap<String, Card>(); private List<CardSet> mCardSets = new ArrayList<CardSet>(); private SparseIntArray mNumPlayerPoints = new SparseIntArray(); private SparseIntArray mDifficultyScale = new SparseIntArray(); private Map<String, String> mNameConversions = new HashMap<String, String>(); private Map<Card, Set<Card>> mAlternates = new HashMap<Card, Set<Card>>(); // Just used during parsing; if needed this can be more robust private Map<Card, CardSet> mReverseCardSetCache = new HashMap<Card, CardSet>(); private Map<Card, String> mTeams = new HashMap<Card, String>(); private int mMinDifficultyPoints; private int mMaxDifficultyPoints; public static List<CardSet> getCardSets() { return sInstance.mCardSets; } /** * Returns all cards of a particular type. Only returns enabled cards. */ public static List<Card> getCards(Type type) { List<Card> cards = new ArrayList<Card>(); for (Card card : sInstance.mCards.values()) { if (card.getType() == type && card.isEnabled()) { cards.add(card); } } return cards; } public static Set<Card> getCardAndAlternates(Card card) { if (sInstance.mAlternates.containsKey(card)) { return sInstance.mAlternates.get(card); } else { // Return a set consisting of just the card itself Set<Card> cards = new HashSet<Card>(); cards.add(card); return cards; } } public static void saveCardStates(Context context) { long start = System.nanoTime(); Set<String> enabledIds = new HashSet<String>(); for (Card card : sInstance.mCards.values()) { if (card.isEnabled()) { enabledIds.add(card.getId()); } } SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); Editor editor = prefs.edit(); editor.putStringSet(PREFERENCE_CARD_STATE, enabledIds); editor.apply(); long end = System.nanoTime(); Log.d("Saved card states in " + ((end - start) / 100000) + " ms"); } public static int getPointsForNumPlayers(int numPlayers) { return sInstance.mNumPlayerPoints.get(numPlayers, 0); } /** * Returns the average points v * * @param type * @return */ public static int getAvgPoints(Type type) { int total = 0; List<Card> cards = getCards(type); for (Card card : cards) { total += card.getPoints(); } return total / cards.size(); } public static int getWinPercent(int points) { // Round points to the nearest 5, as the scale is in 5s. Round up. int mod = points % 5; if (mod >= 3) { points += 5 - mod; } else { points -= mod; } // Search, heading towards 0, until you find something on the difficulty scale int lossPct = -1; do { lossPct = sInstance.mDifficultyScale.get(points, -1); points += points > 0 ? -5 : 5; } while (lossPct == -1); return 100 - lossPct; } /** * Calculates the range of point values that will satisfy the min/max win percents. */ public static Pair<Integer, Integer> getPointRange(int minWinPercent, int maxWinPercent) { if (minWinPercent < 0) { minWinPercent = 0; } if (maxWinPercent > 100) { maxWinPercent = 100; } int start = sInstance.mMaxDifficultyPoints; int end = sInstance.mMinDifficultyPoints; // Search through the entire difficulty scale looking for // the closest value to the min/max win percent. int size = sInstance.mDifficultyScale.size(); int closestMin = 200; int closestMax = 200; for (int index = 0; index < size; index++) { int points = sInstance.mDifficultyScale.keyAt(index); int winPct = 100 - sInstance.mDifficultyScale.get(points); int minDiff = Math.abs(minWinPercent - winPct); if (minDiff < closestMin) { start = points; closestMin = minDiff; } else if (minDiff == closestMin && points > start) { start = points; } int maxDiff = Math.abs(maxWinPercent - winPct); if (maxDiff < closestMax) { end = points; closestMax = maxDiff; } else if (maxDiff == closestMax && points < end) { end = points; } } return new Pair<Integer, Integer>(end, start); } ////////////////////////////////////////////////////////////////////////// // Read data private static final String CARD_FILE = "cards.json"; private static final String NAME_FILE = "names.json"; private static final String POINT_FILE = "points.json"; private void initCards(Context context) throws IOException { InputStream in = null; try { in = context.getAssets().open(CARD_FILE); JsonReader reader = new JsonReader(new InputStreamReader(in)); // Read through top level objects reader.beginObject(); while (reader.hasNext()) { String name = reader.nextName(); if (name.equals("sets")) { reader.beginArray(); while (reader.hasNext()) { mTeams.clear(); CardSet set = readSet(reader); mCardSets.add(set); for (Card card : set.getCards()) { mCards.put(card.getId(), card); mReverseCardSetCache.put(card, set); } for (Card card : mTeams.keySet()) { Card team = mCards.get(mTeams.get(card)); team.addTeamMember(card); } } reader.endArray(); } else if (name.equals("alternates")) { readAlternates(reader); } else { reader.skipValue(); } } reader.endObject(); } finally { in.close(); } } private void readAlternates(JsonReader reader) throws IOException { reader.beginArray(); while (reader.hasNext()) { Set<Card> altSet = new HashSet<Card>(); reader.beginArray(); while (reader.hasNext()) { altSet.add(mCards.get(reader.nextString())); } reader.endArray(); for (Card card : altSet) { mAlternates.put(card, altSet); } } reader.endArray(); } private void addAlternate(Card baseCard, Card altCard) { Set<Card> altSet = mAlternates.get(baseCard); if (altSet == null) { altSet = new HashSet<Card>(); mAlternates.put(baseCard, altSet); } altSet.add(altCard); mAlternates.put(altCard, altSet); } private CardSet readSet(JsonReader reader) throws IOException { CardSet set = new CardSet(); reader.beginObject(); while (reader.hasNext()) { String name = reader.nextName(); if (name.equals("id")) { set.setId(reader.nextString()); } else if (name.equals("name")) { set.setNameResId(ResourceUtils.getIdentifier(R.string.class, reader.nextString())); } else if (name.equals("enabledByDefault")) { set.setEnabledByDefault(reader.nextBoolean()); } else if (name.equals("cards")) { reader.beginArray(); while (reader.hasNext()) { Card card = readCard(reader); // If this card is part of a team, let that team have it, rather than adding it to the set if (!mTeams.containsKey(card)) { set.addCard(card); } } reader.endArray(); } else { reader.skipValue(); } } reader.endObject(); return set; } private Card readCard(JsonReader reader) throws IOException { Card card = new Card(); reader.beginObject(); while (reader.hasNext()) { String jsonName = reader.nextName(); if (jsonName.equals("id")) { card.setId(reader.nextString()); } else if (jsonName.equals("name")) { String name = reader.nextString(); card.setNameResId(ResourceUtils.getIdentifier(R.string.class, name)); } else if (jsonName.equals("icon")) { String icon = reader.nextString(); card.setIconResId(ResourceUtils.getIdentifier(R.drawable.class, icon)); } else if (jsonName.equals("type")) { String type = reader.nextString(); if (type.equals("hero")) { card.setType(Type.HERO); } else if (type.equals("villain")) { card.setType(Type.VILLAIN); } else if (type.equals("environment")) { card.setType(Type.ENVIRONMENT); } } else if (jsonName.equals("team")) { // Put it into a hash for later mTeams.put(card, reader.nextString()); } else { reader.skipValue(); } } reader.endObject(); return card; } private void initNameConversions(Context context) throws IOException { InputStream in = null; JsonReader reader = null; try { in = context.getAssets().open(NAME_FILE); reader = new JsonReader(new InputStreamReader(in)); reader.beginObject(); while (reader.hasNext()) { String name = reader.nextName(); String value = reader.nextString(); mNameConversions.put(name, value); } reader.endObject(); } finally { if (in != null) { in.close(); } if (reader != null) { reader.close(); } } } private void initPoints(Context context) throws IOException { boolean success = initPoints(context, false); // If we failed to load data from whatever we loaded off the network, // fallback to the version we store locally. if (!success) { initPoints(context, true); } } private boolean initPoints(Context context, boolean useAssetFile) { InputStream in = null; JsonReader reader = null; try { // Use the sync file if it exists; otherwise use the built-in points file File syncFile = context.getFileStreamPath(SYNCED_POINT_FILE); if (!useAssetFile && syncFile.exists()) { in = context.openFileInput(SYNCED_POINT_FILE); Log.d("Loading point data from synced file..."); } else { in = context.getAssets().open(POINT_FILE); Log.d("Loading point data from built-in asset file..."); } reader = new JsonReader(new InputStreamReader(in)); try { reader.beginObject(); while (reader.hasNext()) { String name = reader.nextName(); if (name.equals("difficulty")) { readPoints(reader); } else if (name.equals("scale")) { readDifficultyScale(reader); } else { reader.skipValue(); } } reader.endObject(); } finally { if (in != null) { in.close(); } if (reader != null) { reader.close(); } } } catch (IOException e) { Log.w("Could not read point file", e); return false; } return true; } private void readPoints(JsonReader reader) throws IOException { reader.beginObject(); while (reader.hasNext()) { String name = reader.nextName(); if (name.equals("hero") || name.equals("villain") || name.equals("env")) { reader.beginArray(); while (reader.hasNext()) { readCardPoints(reader); } reader.endArray(); } else if (name.equals("nump")) { reader.beginArray(); while (reader.hasNext()) { readNumPlayers(reader); } reader.endArray(); } else { reader.skipValue(); } } reader.endObject(); } private void readCardPoints(JsonReader reader) throws IOException { String cardName = null; int points = 0; Integer advancedPoints = null; int advancedCount = 0; reader.beginObject(); while (reader.hasNext()) { String name = reader.nextName(); if (name.equals("name")) { cardName = reader.nextString(); } else if (name.equals("points")) { points = reader.nextInt(); } else if (name.equals("advanced")) { advancedPoints = reader.nextInt(); } else if (name.equals("advcount")) { advancedCount = reader.nextInt(); } else { reader.skipValue(); } } reader.endObject(); if (mNameConversions.containsKey(cardName)) { String newName = mNameConversions.get(cardName); Log.v("Converting points card \"" + cardName + "\" to \"" + newName + "\""); cardName = newName; } Card card = mCards.get(cardName); if (card == null) { // Sanity check Log.e("Could not find card \"" + cardName + "\" for points"); return; } card.setPoints(points); if (advancedPoints != null) { card.setAdvancedPoints(advancedPoints); card.setAdvancedCount(advancedCount); } } private void readNumPlayers(JsonReader reader) throws IOException { String numPlayers = null; int points = 0; reader.beginObject(); while (reader.hasNext()) { String name = reader.nextName(); if (name.equals("name")) { numPlayers = reader.nextString(); } else if (name.equals("points")) { points = reader.nextInt(); } else { reader.skipValue(); } } reader.endObject(); if (numPlayers.equals("Three")) { mNumPlayerPoints.put(3, points); } else if (numPlayers.equals("Four")) { mNumPlayerPoints.put(4, points); } else if (numPlayers.equals("Five")) { mNumPlayerPoints.put(5, points); } else { throw new RuntimeException("Unknown # of players: " + numPlayers); } } private void readDifficultyScale(JsonReader reader) throws IOException { mMinDifficultyPoints = Integer.MAX_VALUE; mMaxDifficultyPoints = Integer.MIN_VALUE; reader.beginArray(); while (reader.hasNext()) { int total = 0; int lossPercent = 0; reader.beginObject(); while (reader.hasNext()) { String name = reader.nextName(); if (name.equals("total")) { total = reader.nextInt(); } else if (name.equals("losspct")) { lossPercent = reader.nextInt(); } else { reader.skipValue(); } } reader.endObject(); mDifficultyScale.put(total, lossPercent); if (total < mMinDifficultyPoints) { mMinDifficultyPoints = total; } if (total > mMaxDifficultyPoints) { mMaxDifficultyPoints = total; } } reader.endArray(); } ////////////////////////////////////////////////////////////////////////// // Network updates private static final String SYNCED_POINT_FILE = "synced-points.json"; private static final String TMP_SYNCED_POINT_FILE = "synced-points.json.tmp"; private static final String SYNC_POINT_URL = "http://x.gray.org/sentinels.json"; public static boolean updatePoints(Context context) { try { // Clear the old TMP file File tmpFile = context.getFileStreamPath(TMP_SYNCED_POINT_FILE); if (tmpFile.exists()) { Log.v("Deleted old TMP sync download file"); tmpFile.delete(); } // Read JSON to TMP file Log.v("Loading latest points JSON into TMP file"); URL url = new URL(SYNC_POINT_URL); HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection(); urlConnection.setRequestMethod("GET"); urlConnection.connect(); FileOutputStream out = context.openFileOutput(TMP_SYNCED_POINT_FILE, Context.MODE_PRIVATE); InputStream in = urlConnection.getInputStream(); try { byte[] buffer = new byte[1024]; int len = 0; while ((len = in.read(buffer)) > 0) { out.write(buffer, 0, len); } } finally { in.close(); out.close(); } // Rename the TMP file to the sync file Log.v("Renaming TMP sync file to POINT sync file"); tmpFile.renameTo(context.getFileStreamPath(SYNCED_POINT_FILE)); // Reload the new points file Log.v("Reloading points data"); sInstance.initPoints(context); } catch (MalformedURLException e) { // Ignore; this should never happen return false; } catch (IOException e) { Log.w("Could not sync Sentinels data", e); return false; } return true; } }